// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using Mono.Cecil;


namespace Mono.Linker
{
    /// <summary>
    /// Class which implements IDependencyRecorder and writes the dependencies into an DGML file.
    /// </summary>
    public class DependencyRecorderHelper
    {
        static bool IsAssemblyBound(TypeDefinition td)
        {
            do
            {
                if (td.IsNestedPrivate || td.IsNestedAssembly || td.IsNestedFamilyAndAssembly)
                    return true;

                td = td.DeclaringType;
            } while (td != null);

            return false;
        }

        public static string TokenString(LinkContext context, object? o)
        {
            if (o == null)
                return "N:null";

            if (o is TypeReference t)
            {
                bool addAssembly = true;
                var td = context.TryResolve(t);

                if (td != null)
                {
                    addAssembly = td.IsNotPublic || IsAssemblyBound(td);
                    t = td;
                }

                var addition = addAssembly ? $":{t.Module}" : "";

                return $"{((IMetadataTokenProvider)o).MetadataToken.TokenType}:{o}{addition}";
            }

            if (o is IMetadataTokenProvider provider)
                return provider.MetadataToken.TokenType + ":" + o;

            return "Other:" + o;
        }

        static bool ShouldRecordAssembly(LinkContext context, AssemblyDefinition assembly)
        {
            Debug.Assert(context.EnableReducedTracing || context.TraceAssembly != null);

            if (context.TraceAssembly != null && !context.TraceAssembly.Contains(assembly.Name.Name))
                return false; // We were asked to only trace a specific set of assemblies and this is not one of them

            // We were either asked to trace in general, or to trace this assembly in particular.

            // Note: with reduced tracing, we may still not trace an assembly if it's not linked.
            if (context.EnableReducedTracing)
            {
                switch (context.Annotations.GetAction(assembly))
                {
                    case AssemblyAction.Link:
                    case AssemblyAction.AddBypassNGen:
                    case AssemblyAction.AddBypassNGenUsed:
                        return true;
                    default:
                        return false;
                }
            }

            return true;
        }

        public static bool ShouldRecord(LinkContext context, object? source, object target)
        {
            if (source == null || target == null)
                return false;

            // We use a few hacks to work around MarkStep outputting thousands of edges even
            // with the above ShouldRecord checks. Ideally we would format these into a meaningful format
            // however I don't think that is worth the effort at the moment.

            // Prevent useless logging of attributes like `e="Other:Mono.Cecil.CustomAttribute"`.
            if (source is CustomAttribute || target is CustomAttribute)
                return false;

            // Prevent useless logging of interface implementations like `e="InterfaceImpl:Mono.Cecil.InterfaceImplementation"`.
            if (source is InterfaceImplementation || target is InterfaceImplementation)
                return false;

            if (!ShouldRecord(context, source) && !ShouldRecord(context, target))
            {
                return false;
            }
            return true;
        }

        public static bool ShouldRecord(LinkContext context, object? o)
        {
            // If tracing a specific set of assemblies (TraceAssembly != null),
            // this takes precedence over the EnableReducedTracing setting.
            if (!context.EnableReducedTracing && context.TraceAssembly == null)
                return true;

            if (o is TypeDefinition t)
                return ShouldRecordAssembly(context, t.Module.Assembly);

            if (o is IMemberDefinition m)
                return ShouldRecordAssembly(context, m.DeclaringType.Module.Assembly);

            if (o is TypeReference typeRef)
            {
                var resolved = context.TryResolve(typeRef);

                // Err on the side of caution if we can't resolve
                if (resolved == null)
                    return true;

                return ShouldRecordAssembly(context, resolved.Module.Assembly);
            }

            if (o is MemberReference mRef)
            {
                var resolved = mRef.Resolve();

                // Err on the side of caution if we can't resolve
                if (resolved == null)
                    return true;

                return ShouldRecordAssembly(context, resolved.DeclaringType.Module.Assembly);
            }

            if (o is ModuleDefinition module)
                return ShouldRecordAssembly(context, module.Assembly);

            if (o is AssemblyDefinition assembly)
                return ShouldRecordAssembly(context, assembly);

            if (o is ParameterDefinition parameter)
            {
                if (parameter.Method is MethodDefinition parameterMethodDefinition)
                    return ShouldRecordAssembly(context, parameterMethodDefinition.DeclaringType.Module.Assembly);
            }

            return true;
        }
    }
}
